/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package rlpw.lang;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.xml.parsers.*;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

import rlpw.util.Pair;
import rlpw.util.UniqueList;

/**
 * A class for loading and saving of simple objects from or into a xml file.
 *
 * <h3>Example:</h3>
 * <p>Object classes:
 * <pre>
 * {@code
 * class A {
 *  private int n;
 *  private B b;
 *
 *      public B getB() {
 *          return b;
 *      }
 * }
 *
 * class B {
 *   private String str;
 *
 *      public String getStr() {
 *          return str;
 *      }
 * }
 * }
 * </pre>
 * XML File:
 * <pre>
 * {@code
 * <?xml version="1.0" encoding="UTF-8"?>
 *  <root>
 *    <class name="A">
 *       <instance name="a1">
 *           <field name="n" value="10"/>
 *           <field name="b" ref="b2"/>
 *       </instance>
 *    </class>
 *    <class name="B">
 *       <instance name="b2">
 *           <field name="str" value="Hello World"/>
 *       </instance>
 *    </class>
 *  </root>
 * }
 * </pre>
 * Implementation:
 * <pre>
 * {@code
 * XMLClassResource xmlCR = new XMLClassRessource(file);
 * A a = xmlCR.getInstance(A.class, "a1");
 *
 * System.out.println(a.getB().getStr());
 * // Hello World
 *
 *
 * }
 * </pre>
 * @author weinpau
 */
public class XMLClassResource {

    public static final String XML_ROOT = "root";
    public static final String XML_TAG_CLASS = "class";
    public static final String XML_TAG_INSTANCE = "instance";
    public static final String XML_TAG_FIELD = "field";
    public static final String XML_TAG_ITEM = "item";
    public static final String XML_ATTRIBUTE_NAME = "name";
    public static final String XML_ATTRIBUTE_VALUE = "value";
    public static final String XML_ATTRIBUTE_REFERENCE = "ref";
    private InputStream is;
    private HashMap<Class, HashMap<String, Pair<Object, NodeList>>> objs = new HashMap<Class, HashMap<String, Pair<Object, NodeList>>>();
    private XMLParser parser;

    public XMLClassResource() throws ParserConfigurationException {
        parser = new XMLParser();
        parser.getDocument().insertBefore(parser.getDocument().createElement(XML_ROOT), null);
    }

    public XMLClassResource(InputStream is) throws IOException, ParserConfigurationException, SAXException {
        this.is = is;
        parser = new XMLParser(getInputStream());
    }

    public XMLClassResource(File file) throws FileNotFoundException, IOException, ParserConfigurationException, SAXException {
        is = new FileInputStream(file);
        parser = new XMLParser(getInputStream());
    }

    public InputStream getInputStream() {
        return is;
    }

    public String getNameOf(Object obj) {
        for (Class c : objs.keySet()) {
            for (String name : objs.get(c).keySet()) {
                if (objs.get(c).get(name).left() == obj) {
                    return name;
                }
            }
        }
        return null;
    }

    public List<String> getNamesOf(Class c) {

        List<String> list = new UniqueList<String>();
        NodeList nl = parser.getNodeListByTagName(XML_TAG_INSTANCE);
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);


            if (node.getParentNode().getAttributes().getNamedItem(XML_ATTRIBUTE_NAME).getNodeValue().equals(c.getName())) {
                list.add(node.getAttributes().getNamedItem(XML_ATTRIBUTE_NAME).getNodeValue());
            }
        }

        Collections.sort(list,new AlphanumComparator());

        return list;
    }

    public <T> List<T> getAllObjectsOf(Class<T> c) {

        List result = new ArrayList();
        for (String name : getNamesOf(c)) {
            try {
                result.add(getInstance(c, name));
            } catch (Exception ex) {
            }
        }
        return result;
    }

    public boolean contains(Object obj) {
        return getNameOf(obj) != null;
    }

    public void saveTo(File file) throws TransformerConfigurationException, FileNotFoundException, TransformerException, IOException {

        for (Class c : objs.keySet()) {
            for (String name : objs.get(c).keySet()) {
                Pair<Object, NodeList> pair = objs.get(c).get(name);
                NodeList elms = pair.right();
                Object obj = pair.left();

                for (int i = 0; i < elms.getLength(); i++) {
                    try {
                        if (elms.item(i).getNodeName().equals(XML_TAG_FIELD)) {

                            Field field = obj.getClass().getDeclaredField(elms.item(i).getAttributes().getNamedItem(XML_ATTRIBUTE_NAME).getNodeValue());
                            field.setAccessible(true);
                            Object content = field.get(obj);

                            if (field.getType().isArray()) {

                                // Arrayinhalt löschen
                                NodeList nl = elms.item(i).getChildNodes();
                                List<Node> ls = new ArrayList<Node>();
                                for (int j = 0; j < nl.getLength(); j++) {
                                    if (nl.item(j).getNodeName().equals(XML_TAG_ITEM)) {
                                        ls.add(nl.item(j));
                                    }
                                }
                                for (Node n : ls) {
                                    elms.item(i).removeChild(n);
                                }

                                // Array neu machen
                                Object array = field.get(obj);

                                for (int j = 0; j < Array.getLength(array); j++) {

                                    Element item = parser.getDocument().createElement(XML_TAG_ITEM);
                                    if (StringParser.isParsable(field.getType().getComponentType())) {
                                        item.setAttribute(XML_ATTRIBUTE_VALUE, StringParser.getString(Array.get(array, j)));
                                    } else {
                                        // Referenz auf ein anderes Object setzen
                                        Object newObj = Array.get(array, j);
                                        putInstance(newObj);
                                        item.setAttribute(XML_ATTRIBUTE_REFERENCE, getNameOf(newObj));
                                    }

                                    elms.item(i).insertBefore(item, null);
                                }


                            } else if (StringParser.isParsable(field.getType())) {
                                try {
                                    elms.item(i).getAttributes().getNamedItem(XML_ATTRIBUTE_VALUE).setNodeValue(StringParser.getString(content));
                                } catch (NullPointerException ex) {
                                }
                            } else {
                                Object newObj = field.get(obj);
                                putInstance(newObj);
                                elms.item(i).getAttributes().getNamedItem(XML_ATTRIBUTE_REFERENCE).setNodeValue(getNameOf(content));
                            }

                        }
                    } catch (Exception excep) {
                    }
                }
            }
        }

        parser.getDocument().normalizeDocument();

        DOMSource source = new DOMSource(parser.getDocument());

        FileOutputStream os = new FileOutputStream(file);
        StreamResult result = new StreamResult(os);

        String xsl = "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">"
                + "<xsl:output method=\"xml\" indent=\"yes\"/>"
                + "<xsl:strip-space elements=\"*\"/>"
                + "<xsl:template match=\"/\">"
                + "<xsl:copy-of select=\".\"/>"
                + "</xsl:template>"
                + "</xsl:stylesheet>";

        Transformer transformer = TransformerFactory.newInstance().newTransformer(
                new StreamSource(new StringReader(xsl)));

        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

        transformer.transform(source, result);

        os.close();

    }

    public <T> T getInstance(Class<T> c, String name) throws InstantiationException, IllegalAccessException, NoSuchFieldException {

        if (objs.containsKey(c) && objs.get(c).containsKey(name)) {
            return (T) objs.get(c).get(name).left();
        }

        NodeList nl = parser.getNodeListByTagName(XML_TAG_INSTANCE);
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            if (node.getParentNode().getNodeName().equals(XML_TAG_CLASS)
                    && node.getParentNode().getAttributes().getNamedItem(XML_ATTRIBUTE_NAME).getNodeValue().equals(c.getName())
                    && node.getAttributes().getNamedItem(XML_ATTRIBUTE_NAME).getNodeValue().equals(name)) {


                NodeList elms = node.getChildNodes();

                Object obj = c.newInstance();

                for (int j = 0; j < elms.getLength(); j++) {
                    if (elms.item(j).getNodeName().equals(XML_TAG_FIELD)) {

                        String fieldname = elms.item(j).getAttributes().getNamedItem(XML_ATTRIBUTE_NAME).getNodeValue();
                        Field field = obj.getClass().getDeclaredField(fieldname);
                        field.setAccessible(true);
                        Class type = field.getType();

                        if (type.isArray()) {

                            NodeList arr_items = elms.item(j).getChildNodes();

                            //Länge des Arrays bestimmen
                            int size = 0;
                            for (int k = 0; k < arr_items.getLength(); k++) {
                                if (arr_items.item(k).getNodeName().equals(XML_TAG_ITEM)) {
                                    size++;
                                }
                            }

                            Object array = Array.newInstance(type.getComponentType(), size);
                            int index = 0;

                            for (int k = 0; k < arr_items.getLength(); k++) {

                                if (arr_items.item(k).getNodeName().equals(XML_TAG_ITEM)) {

                                    if (StringParser.isParsable(type.getComponentType())) {
                                        try {
                                            Array.set(array, index, StringParser.parse(type.getComponentType(), arr_items.item(k).getAttributes().getNamedItem(XML_ATTRIBUTE_VALUE).getNodeValue()));
                                        } catch (Exception excep) {
                                        }
                                    } else {
                                        try {
                                            String ref = arr_items.item(k).getAttributes().getNamedItem(XML_ATTRIBUTE_REFERENCE).getNodeValue();
                                            if (ref.isEmpty()) {
                                                Array.set(array, index, null);
                                            } else {
                                                Array.set(array, index, getInstance(type.getComponentType(), ref));
                                            }
                                        } catch (NullPointerException ex) {
                                        }
                                    }
                                    index++;
                                }
                            }

                            field.set(obj, array);

                        } else if (StringParser.isParsable(type)) {
                            try {
                                field.set(obj, StringParser.parse(type, elms.item(j).getAttributes().getNamedItem(XML_ATTRIBUTE_VALUE).getNodeValue()));
                            } catch (Exception ex) {
                            }
                        } else {
                            // Referenz auf ein anderes Object
                            try {
                                String ref = elms.item(j).getAttributes().getNamedItem(XML_ATTRIBUTE_REFERENCE).getNodeValue();
                                if (ref.isEmpty()) {
                                    field.set(obj, null);
                                } else {
                                    field.set(obj, getInstance(type, ref));
                                }
                            } catch (NullPointerException ex) {
                            }
                        }
                    }
                }

                if (!objs.containsKey(c)) {
                    objs.put(c, new HashMap<String, Pair<Object, NodeList>>());
                }
                objs.get(c).put(name, new Pair<Object, NodeList>(obj, elms));
                return (T) obj;

            }

        }

        throw new InstantiationException("Can't create a new instance of " + c.getName());

    }

    public void removeInstance(Object obj) {
        NodeList nl = parser.getNodeListByTagName(XML_TAG_INSTANCE);
        String name = getNameOf(obj);

        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            if (node.getParentNode().getNodeName().equals(XML_TAG_CLASS)
                    && node.getParentNode().getAttributes().getNamedItem(XML_ATTRIBUTE_NAME).getNodeValue().equals(obj.getClass().getName())
                    && node.getAttributes().getNamedItem(XML_ATTRIBUTE_NAME).getNodeValue().equals(name)) {

                node.getParentNode().removeChild(node);
                objs.get(obj.getClass()).remove(name);
            }
        }
    }

    public void putInstance(Object obj) throws IllegalArgumentException, IllegalAccessException, InstantiationException, NoSuchFieldException {

        Class c = obj.getClass();

        if (!objs.containsKey(c)) {
            putInstance(obj, c.getSimpleName().toLowerCase());
        }
        int id = 1;
        while (objs.get(c).containsKey(c.getSimpleName().toLowerCase() + id++));

        putInstance(obj, c.getSimpleName().toLowerCase() + id);
    }

    public void putInstance(Object obj, String name) throws IllegalArgumentException, IllegalAccessException, InstantiationException, NoSuchFieldException {

        Class c = obj.getClass();

        if (getNamesOf(c).contains(name)) {
            removeInstance(getInstance(c, name));
        }

        if (contains(obj)) {
            return;
        }

        NodeList class_nl = parser.getNodeListByTagName(XML_TAG_CLASS);

        Node classNode = null;

        for (int i = 0; i < class_nl.getLength(); i++) {
            if (class_nl.item(i).getAttributes().getNamedItem(XML_ATTRIBUTE_NAME).getNodeValue().equals(c.getName())) {
                classNode = class_nl.item(i);
            }
        }

        if (classNode == null) {
            Element newClass = parser.getDocument().createElement(XML_TAG_CLASS);
            newClass.setAttribute(XML_ATTRIBUTE_NAME, c.getName());
            Node rootNode = parser.getDocument().getDocumentElement();
            rootNode.insertBefore(newClass, null);
            classNode = newClass;
        }


        Element newInstance = parser.getDocument().createElement(XML_TAG_INSTANCE);
        newInstance.setAttribute(XML_ATTRIBUTE_NAME, name);

        if (!objs.containsKey(c)) {
            objs.put(c, new HashMap<String, Pair<Object, NodeList>>());
        }
        objs.get(c).put(name, new Pair<Object, NodeList>(obj, newInstance.getChildNodes()));

        // Felder hinzufuegen:

        for (Field field : c.getDeclaredFields()) {
            try {
                Class type = field.getType();
                field.setAccessible(true);


                Element newField = parser.getDocument().createElement(XML_TAG_FIELD);
                newField.setAttribute(XML_ATTRIBUTE_NAME, field.getName());

                if (type.isArray()) {

                    Object array = field.get(obj);
                    for (int k = 0; k < Array.getLength(array); k++) {
                        Element item = parser.getDocument().createElement(XML_TAG_ITEM);
                        if (StringParser.isParsable(type.getComponentType())) {
                            item.setAttribute(XML_ATTRIBUTE_VALUE, StringParser.getString(Array.get(array, k)));
                        } else {
                            // Referenz auf ein anderes Object setzen
                            Object newObj = Array.get(array, k);
                            putInstance(newObj);
                            item.setAttribute(XML_ATTRIBUTE_REFERENCE, getNameOf(newObj));
                        }

                        newField.insertBefore(item, null);
                    }

                } else if (StringParser.isParsable(type)) {
                    newField.setAttribute(XML_ATTRIBUTE_VALUE, StringParser.getString(field.get(obj)));
                } else {
                    // Referenz auf ein anderes Object setzen
                    Object newObj = field.get(obj);
                    putInstance(newObj);
                    newField.setAttribute(XML_ATTRIBUTE_REFERENCE, getNameOf(newObj));
                }

                newInstance.insertBefore(newField, null);
            } catch (NullPointerException excep) {
            }
        }

        classNode.insertBefore(newInstance, null);

    }

    private class XMLParser {

        private Document document;

        public XMLParser(InputStream is) throws IOException, ParserConfigurationException, SAXException {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            document = factory.newDocumentBuilder().parse(is);

        }

        public XMLParser() throws ParserConfigurationException {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            document = factory.newDocumentBuilder().newDocument();
        }

        public NodeList getChildNodeList() {
            Element ele = document.getDocumentElement();
            return ele.getChildNodes();
        }

        public NodeList getNodeListByTagName(String name) {
            Element ele = document.getDocumentElement();
            return ele.getElementsByTagName(name);
        }

        public Document getDocument() {
            return document;
        }

        public void setDocument(Document document) {
            this.document = document;
        }
    }
}
